#include <assert.h>
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <memory.h>
#include <netinet/in.h>
#include <unistd.h>
#include <fcntl.h>

#include <iostream>

#include <sys/types.h>
#include <sys/stat.h>

#include <fcntl.h>
#include <errno.h>

#include <stdlib.h>
#ifndef _MAX_PATH
#define _MAX_PATH 1024
#endif

#include "ptypes.h"
#include "rabinfp.h"
#include "packeteer.h"
#include <vector>
#include "cacheindex.h"

#include "SimpleSqliteConfig.h"

/*
  Cache (dictionary) can reside in a disk file, in memory, or on raw disk partition
  Each dictionary is split in half, so it can handle encoding outbound packets and
  decoding inbound, so there are really two dictionaries in one glass.
  Encoding dictionary must be in sync with decoding dictionary on the peer, and
  decoding dictionary muyst be in sync with peer's encoding one, otherwise we have zhopa.
  Each chunk of data sent to the other side must contain a verdict generated by the encoder,
  which instructs the other side how to treat this chunk.
*/

bool DiskCache::Initialize(class Packeteer *parent, int context, const char *db_name) {
  int ret;
  int openFlag;
  DiskCacheHeader header;

  if (NULL == db_name)
    return false;

  bool createCache;
  std::string fN;

  // mandatory fields
  try {
    printf("Opening connection to configuration database %s\n", db_name);
    auto cnf = new Conf::SimpleSqliteConfig("DRECompressor", db_name);
    m_memoryMode = cnf->getBool("MemoryMode");
    m_rawdiskMode = cnf->getBool("RawDiskMode");
    fN = cnf->getStr("FileName");
    if (m_rawdiskMode) {
      m_rawBlocks = cnf->getInt("RawBlocks");
      m_blockSize = cnf->getInt("RawBlocksSize");
    }
    createCache = cnf->getBool("CreateCache");
    if (createCache)
      m_header.bits = cnf->getInt("DiskCacheBits");
    m_header.guardRecords = cnf->getInt("GuardRecords");
    assert (m_header.guardRecords > 1);
    m_header.quarantinRecords = cnf->getInt("QuarantineRecords");

    // optional fields
    try {
      m_collectChunkStats = cnf->getBool("CollectChunkStatistics");
    } catch (const Conf::SqliteConfigEx  &e) {
      m_collectChunkStats = false;
    }
    try {
      m_readOnlyCache = cnf->getBool("ReadOnlyCache");
    } catch (const Conf::SqliteConfigEx  &e) {
      m_readOnlyCache = false;
    }
    try {
      m_penalty = cnf->getInt("MinSpan");
    } catch (const Conf::SqliteConfigEx  &e) {
      m_penalty = 20;
    }
    try {
      m_purgingPoolThreshold = cnf->getFloat("PurgingPoolThreshold");
    } catch (const Conf::SqliteConfigEx  &e) {
      m_purgingPoolThreshold = 0.5;
    }
    try {
      m_redundancyThreshold = cnf->getFloat("RedundancyThreshold");
    } catch (const Conf::SqliteConfigEx  &e) {
      m_redundancyThreshold = 0.3;
    }

    delete cnf;
  } catch (const char* e) {
    std::cerr << "ERROR: " << e << std::endl;
    return false;
  } catch (const std::string &e) {
    std::cerr << "ERROR: " << e << std::endl;
    return false;
  } catch (const Conf::SqliteConfigEx  &e) {
    std::cerr << "SqliteConfigEx:" << std::endl
              << "  " << e.kind << std::endl
              << "  " << e.type << std::endl
              << "  " << e.msg  << std::endl;
    return false;
  } catch (...) {
    std::cerr << "UNKNOWN ERROR" << std::endl;
    return false;
  }

  m_parent = parent;
  m_firstTime = true;
  m_header.inboundFirstSave =
	  m_header.outboundFirstSave = true;

  m_context = context;

  int raw_size;

  // TODO: add ability to warm up memory or rawdisk cache from separate disk cache
  if (m_memoryMode) {
    raw_size = 1 <<  m_header.bits;
    m_handle = (long)(new unsigned char[raw_size + sizeof(long)]);
    if (0 == m_handle) {
      printf("Error allocating %ld bytes for ramcache\n", raw_size + sizeof(long));
      return false;
    }
    m_half = raw_size/2;

    Chunk chunk(m_parent);
    m_recordSize = chunk.GetSizeOnDisk();

    m_oneWayRecords = m_half / m_recordSize;

    m_header.inboundFirstRecord =  m_header.inboundLastRecord = -1;

    m_header.outboundFirstRecord = m_header.outboundLastRecord = -1;

    m_header.signature = DCH_SIG;

    cacheLseek(m_context * sizeof(m_header), SEEK_SET);
    // write out the header
    if(sizeof(m_header) != cacheWrite(&m_header, sizeof(m_header))) {
      cacheClose();
      assert(0);
      return false;
    }
  }
  else if (m_rawdiskMode) {
    raw_size = m_rawBlocks * m_blockSize;

    const char *fname = fN.c_str();
    openFlag = O_RDWR;
    printf("Initializing raw mode cache (%s) for context %d\n", fname, m_context);
    m_handle = cacheOpen((char *)fname, openFlag, S_IWUSR | S_IRUSR);
    if (-1 == m_handle) {
      printf("Error, errno=%d\r\n", errno);
      return false;
    }

    cacheLseek(m_context * sizeof(m_header), SEEK_SET);
    // attempt reading the header
    if (sizeof(m_header) != cacheRead(&header, sizeof(header))) {
	cacheClose();
	assert(0);
	return false;
    }

    m_half = raw_size/2;

    Chunk chunk(m_parent);
    m_recordSize = chunk.GetSizeOnDisk();

    m_oneWayRecords = m_half / m_recordSize;

    if (DCH_SIG != header.signature) { // not valid header, build cache from scratch

      printf("building new cache header\n");

      m_header.inboundFirstRecord =  m_header.inboundLastRecord = -1;

      m_header.outboundFirstRecord = m_header.outboundLastRecord = -1;

      m_header.signature = DCH_SIG;

      cacheLseek(m_context * sizeof(m_header), SEEK_SET);
      // write out the header
      if(sizeof(m_header) != cacheWrite(&m_header, sizeof(m_header))) {
	cacheClose();
	assert(0);
	return false;
      }
    }
    return true;
  }
  else {
    // Regular file-based cache

    if (m_readOnlyCache)
      openFlag = O_RDONLY;
    else
      openFlag =  O_RDWR | O_CREAT;
    if(createCache) {
      openFlag |= O_TRUNC;
    }

    char fileName[_MAX_PATH];

    sprintf(fileName, "%s_%d", fN.c_str(), m_context);
    printf("Initializing file mode cache (%s), readonly %d\n", fileName, m_readOnlyCache);
    m_handle = cacheOpen(fileName, openFlag, S_IWUSR | S_IRUSR  );  
    if (m_handle == -1) {
      fprintf(stderr, "Error opening file mode cache, errno=%d\r\n", errno);
      return false;
    }

    raw_size=cacheLseek(0, SEEK_END);
   
    if(createCache || raw_size < sizeof(m_header)) {
      raw_size = 1 <<  m_header.bits;
      m_half = raw_size/2;

      Chunk chunk(m_parent);
      m_recordSize = chunk.GetSizeOnDisk();

      m_oneWayRecords = m_half / m_recordSize;

      m_header.inboundFirstRecord =  m_header.inboundLastRecord = -1;

      m_header.outboundFirstRecord = m_header.outboundLastRecord = -1;

      m_header.signature = DCH_SIG;

      cacheLseek(0, SEEK_SET);
      if(sizeof(m_header) != cacheWrite(&m_header, sizeof(m_header))) {
	assert(0);
	return false;
      }

      // preallocate space:

      unsigned char nulls[256];
      memset(nulls, '\0', sizeof(nulls));
      int null_chunks;
      for (null_chunks = 0; null_chunks < raw_size / sizeof(nulls); null_chunks++)
        assert(cacheWrite(nulls, sizeof(nulls)) == sizeof(nulls));
      int left = raw_size - null_chunks * sizeof(nulls);
      if (left > 0)
        assert(cacheWrite(nulls, left) == left);
      cacheLseek(raw_size + sizeof(m_header), SEEK_SET);      
      ret = cacheWrite(&raw_size, 1);
      assert(ret == 1);
      printf("Disk cache header: %ld bytes, chunk size on disk: %d bytes\n", sizeof(m_header), m_recordSize);
      printf("Allocated %d bytes for the new cache to hold %d one-way records\n", raw_size, m_oneWayRecords);
    } else {
      cacheLseek(0, SEEK_SET);
      ret = cacheRead(&m_header, sizeof(m_header));
      assert(ret == sizeof(m_header));
      raw_size = 1 <<  m_header.bits;
      m_half = raw_size/2;
      Chunk chunk(m_parent);
      m_recordSize = chunk.GetSizeOnDisk();
      m_oneWayRecords = m_half / m_recordSize;

      // TODO: inboundFirstRecord and friends don't seem to be updated on disk, stay -1

      // we have to warm the fingerprint map for outbound
      std::cout << "Readonlycache: " << m_readOnlyCache << std::endl;
      std::cout << "CollectChunkStatistics: " << m_collectChunkStats << std::endl;

      std::cout << "Warming up fingerprint index for " << m_oneWayRecords << " records\n";
      for (int recordNum = 0; recordNum < m_oneWayRecords; recordNum++) {
        // TODO: this is inefficient: reads chunks from disk, instead of just the fps,
        // Also, changes m_gets stats. Create separate optimized function
        Chunk *chunk = GetChunk(recordNum + m_oneWayRecords);
        chunk->m_recordNumber = recordNum + m_oneWayRecords;
        chunk->WarmCacheIndex(m_handle);
        delete chunk;
      }
      if (false && m_collectChunkStats)
        m_index.PrintFingerprintMap();
    }
  }
  m_runtimeStats = new ChunkStatistics(m_oneWayRecords);
  return m_handle != -1;
}

DiskCache::DiskCache(){
  m_purges = m_gets = m_saves = 0L;
  m_handle = -1;
  m_runtimeStats = NULL;
};


DiskCache::~DiskCache(){
  PrintChunkStats(false, false); // configure verbosity later
  if(-1 != m_handle) 
    cacheClose();
  if (m_runtimeStats)
    delete m_runtimeStats;
};
